make a lisp 2週目
全体で1400行もない
mjsでの実行のさせかた確認しつつ
ブラウザで動作させたほうが楽しいのでそれの用意と
あとテストだな
ブラウザ上でとりあえずreplっぽい動きは用意できた
mochaでテスト用意
step0
とにかく文字を返す
step1
正規表現をまるっとけずる
const re = /[\s,]*([^\s]*)/g;
数字を整数と小数読む
あとはSymbol.for('hoge')とした
malのテスト見るとdefferable(遅延可能)とそうでないのが分かりやすい
ここで必要なのはリスト
評価はせずにリストとしてparseできるようにしよう
code:diff
カッコ込でちゃんとパースするだけで正規表現クソ大変だった
「または」の順序
[\s,]* 先頭トリム? でもカンマの意味が分からん
ちがうわ、無視する部分か
( a , b)のとき、(, \s(a), \s,\s(b)こういう感じでマッチさせるんだ
/A(B)/というグループで分けて、Bにマッチさせたいやつを和集合で渡して、無視したいのをAにかけばいいのか
後回しにしたもの
nil, true, false
string literal
エラーのテスト
閉じカッコなし
文字列の閉じなし
クオート
'
quote
quasiquote,
unquote ~
splice-unquote ~@
キーワード
ベクタ
ハッシュマップ
コメント
@ / deref
optional
metadata ^
nonalphanumerice characters
コメント後のいろんな文字列
step2
リストの評価
code:js
const eval_ast = (ast, env) => {
if (typeof ast === "symbol") {
if (ast in env) {
} else {
throw Error('${Symbol.keyFor(ast)}' not found);
}
} else if (ast instanceof Array) {
return ast.map((x) => EVAL(x, env));
} else {
return ast;
}
};
const EVAL = (ast, env) => {
if (!isList(ast)) {
return eval_ast(ast, env);
}
if (ast.length === 0) {
return ast;
}
// evaluate function
return f(...args);
};
こんなかんじで評価する
リスト内はそのままmapで評価すれば再帰する
symbolはそのまま
step2の遅延可能なの
配列, hashmapの内側評価
step3
envを外側に出す
new_env
letとか関数の代入をやる
(fn* (a b) (+ a b))
環境を複製して、呼び出し時に渡されたexprをa,bでenvにセットする
なのでまだ関数定義作ってないのにnew_envの中身を先に実装してしまったなmiyamonz.icon
env_get
env_set
EVALでのenvへの副作用
def!
!はなんだろ
副作用あるやつとかにつけるのか
let*
まあ*無しでもいいかな
code:js
case "def":
return env_set(env, a1, EVAL(a2, env));
case "let":
const let_env = new_env(env);
for (let i = 0; i < a1.length; i += 2) {
env_set(let_env, a1i, EVAL(a1i + 1, let_env)); }
return EVAL(a2, let_env);
こんな感じ
defは現在のenvにset
letは新規envを作って評価
let_envはここで使用されて消える
defferable
letの定義部分をvectorでも可能にする
(let [z 1] z)
letの評価部分もvectorを許す
(let (a 5 b 6) [a b])
miyamonz.icon
副作用のあるなしとかはmetadataとして渡せば良いはず(必要なら)
step4
list関数
list
(list) => ()
list?
empty?
count
if
conditinals
=
これは再帰的にやらないといけない
coreに書いた
配列(list)の再帰だけやった
mapの再帰も後で足す
>, <, >=, <=
これは普通にcoreにそのまま実装
fn*
クロージャ
do
ただ単に順番にevalする
prn
これはいいや
step4 のdeffarable
文字の比較、
variable length argument
((fn* (& more) (count more)) 1 2 3)
多分既に動くと思うが
---
step5
tail call optimizatoin
なんかコードが汚くなるからやだな
まあやった
たしかにでかい再帰が実行できた
EVALの関数定義のとこ(fn args exp)
これを、mal上の関数オブジェクトとして定義して末尾呼出最適化できるようにしてある
const fn = () => {}で空っぽにしても動いた
step6 ファイルとか
ファイルを呼んだりするのに文字列使うので、ここまで放置してきた文字列リテラルをやる
文字列リテラル
"これあたりreadでちゃんとparseする
code:js
/"(?:\\.|[^\\"])*"?/
"hoge"をちゃんとparseする
?: カッコのグループをキャプチャしない
Wコロン内の文字はキャプチャしなくていい
\\. メタ文字の認識
\sとか
[^\\"] \ と "以外をキャプチャ(文字列リテラル内の文字)
code:txt
左
ダブルクオートも含めて文字列として投げる
バックスラッシュも文字列
右
正規表現でパースした結果
バックスラッシュも文字列としてパースするので、表示状はエスケープされる
print側
code:js
} else if (typeof obj === "string") {
const str = obj
.replace(/\\/g, String.raw\\)
.replace(/"/g, String.raw\")
.replace(/\n/g, String.raw\n);
return "${str}";
}
エスケープが必要な文字を見つけたら、エスケープした上で表示する
regex上でエスケープが必要であるかどうかとか、
String.rawは完全にそのまま
文字列を扱う上で必要なことは、
読み込み時はメタ文字を解決しつつ、
表示時にはそれを戻す
しかし、メタ文字の解決はmalでは改行しかやってないな まあいいや
コメントアウト
code:diff
右の除外文字に;を入れる
code:js
while ((match = re.exec(str)1) != "") { continue;
}
results.push(match);
}
このcontinueで無視できる
これだけでもいけるのだが、コメント部分をまとめてtokenにするために;.*もパースするようにしてある
malがそうなっている
テストのexpectedがコメントに書いてあるからこれ使ってるのかな?
eval
バグが発生して非常に悩まされた
new_env時に親のenvを固定していたのがだめだった
const e = {...outer}
動的に参照するようにObject.create(outer)して解決
(let () (do (eval (read-string "(def aa 1)")) aa) )
これのevalで、大本のreplのenvにセットされるはずなのだが、let時に上記の{...outer}してしまっているとこの変化が見えないのでaaがnot foundになってしまう
let時点でのenvが固定されてしまう
これ、今は理解できてるけど後で読み返して意味わかるか?
多分大丈夫だと思う
step6
load-file
(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))'
こうなっとる
slurp
外部ファイルをそのまま文字列としてとってくる
malにおいては、nodeならreadFileSync, ブラウザならhttp request
しかしload-fileも後回しでいいか
deferrableではないのだが
atomとderef
どちらもcoreで定義
Atomはそういうクラスを作ってる
code:js
class Atom {
constructor(val) { ths.val = val }
}
こんだけ
derefはatom => atom.valするだけ
resetやswap
swapはオモロイ
(swap! atm + 3)とかができるね
難しいところはないな
step6のdefferable
@ derefのショートハンド
code:cljs
(def! atm (atom 5))
@atm ;=> 5
コメント内の特殊な記号とか
step7
cons
(a,b) => [a, ...b]するだけ
concat
(...a) => a.reduce( (x,y) => x.concat(y), [])
[...x, ...y]でもいい
quote
unquote
splice-unquote
= のsymbolの等価
=自体は実装済み
テスト内でstr使うものがあったので、遅延していたpr-strとstrを実装した
pr-strとstrのprint_readablyの意味がちょっとわかった
正確には追ってない
前に文字列リテラルまわり整えた時やったはずなんだが
jsで実装する場合のString.raw大事
defferable
' quote
` quasiquote
~ unquote
~@ splice-unquote
readerの正規表現に追加して、対応する変換を書いて
code:js
case ':
reader.next();
case "`":
reader.next();
case "~":
reader.next();
case "~@":
reader.next();
こんだけ
ベクタでの動作
これは後回しにしよっと
step8 macro
defmacro
macroexpand
defferable
non-macroな関数
not
これだけやった
nth
first
rest
cond macro
これthrowはいってるからstep8にあるのは間違いやな。step9のあとにやる
step9
throw
try, catch
ここでcondを実装
nth
first
rest
が必要だったので入れた
builtin
symbol?
nil?
true?
false?
apply
map
deferrable
sequential
dissoc
assocって実装したっけ?
ハッシュマップ
stepA
deferrable
meta
with-meta
string?
number?
fn?
macro?
conj
seq
残っている関数がいくつかあるものの、だいたいできた気がする
750Lくらいか。内容が濃いのはあるがおもったより書いてなかったな
以上の実装内容の依存関係をグラフで表示してみたい
formとは、最初の要素が特別なコマンド(関数またはマクロ)であるようなリストのこと